%% proxy behaviour with round robin backend choice
%% and backends failover tracking

-module(gproxy3).
-export([start_be/1,start_gw/1,stop/1,rpc/2, change_code/2]).
%% backends management
-export([rrobin/1, change_elect/2, add_be/2, del_be/2, list_be/1]).

%% using functions, defined in reverl_util.erl
-define(UTILMOD, reverl_util).

-define(INFO(Format, Args),
        ?UTILMOD:info(?MODULE, ?LINE, self(), Format, Args)).
-define(WARN(Format, Args),
        ?UTILMOD:warn(?MODULE, ?LINE, self(), Format, Args)).
-define(ERR(Format, Args),
        ?UTILMOD:error(?MODULE, ?LINE, self(), Format, Args)).

-define(IS_ALIVE(Pid), ?UTILMOD:is_pid_alive(Pid)).


%% simple round robin rotation
rrobin(Pool) ->
  if Pool /= [] ->
    [First|TheRest] = Pool,
    case ?IS_ALIVE(First) of
      true ->
        {First,lists:flatten(TheRest,[First])};
      false ->
        ?WARN("backend ~p is down~n",[First]),
        rrobin(TheRest)
    end;
  true ->
    {empty,[]}
  end.

%% start the server (backend)
start_be(Fun) ->
  spawn(fun() -> loop(Fun) end).

%% start the gateway (frontend)
%% [Pool] - a list of possible backends
start_gw(Pool) when not is_list(Pool) ->
  start_gw([Pool]);
start_gw(Pool) ->
  spawn(fun() -> gloop(Pool,fun(X)->gproxy3:rrobin(X) end) end).

%% stop gateway or backend
stop(Pid) -> 
  Pid ! stop.

%% Pid can be gateway or server 
rpc(Pid, Query) ->
  Pid ! {self(), Query},
  receive
    {Pid, crash} ->
      exit(rpc); 
    {Pid, mustwait} ->
      ?WARN("~p not ready. please wait...~n",[Pid]),
      exit(rpc); 
    {Pid, Why, mustwait} ->
      ?WARN("~p not ready, reason: ~p~n",[Pid,Why]),
      exit(rpc);
    {Pid, Reply} ->
      Reply
  end.

%% function code change
%% send to the SERVER (backend)
change_code(Server, Fun) ->
  Server ! {swap_code,Fun}.

change_elect(Gw, Elect) ->
  Gw ! {swap_elect,Elect}.

add_be(Gw, Server) ->
  Gw ! {be_add, Server}.

del_be(Gw, Server) ->
  Gw ! {be_del, Server}.

list_be(Gw) ->
  Gw ! {self(), list_be},
  receive
    {ok,Reply} -> 
      Reply
  end.

%% server loop
loop(Fun) ->
  receive
    stop ->
      void;
    %% replace the service code
    {swap_code,Fun1} ->
      loop(Fun1);
    %% direct request
    {Client, Data} ->
      case catch Fun(Data) of
        {'EXIT', Why} ->
          ?ERR("Server: ~p, Query: ~p, Reason: ~p~n", [Client, Data, Why]),
          Client ! {self(), crash},
          loop(Fun);
        Reply ->
          Client ! {self(), Reply},
          loop(Fun)
      end;
    %% request via gateway
    {Gw, Client, Data} ->
      case catch Fun(Data) of
        {'EXIT', Why} ->
          ?ERR("Server: ~p, Query: ~p, Reason: ~p~n", [Client, Data, Why]),
          Gw ! {Client, crash},
          loop(Fun);
        Reply ->
          Gw ! {Client, ok, Reply},
          loop(Fun)
      end
  end.

%% gateway loop
gloop(Pool,Elect) ->
  receive
    stop ->
      void;
    %% replace BE election function
    {swap_elect,Elect1} ->
      gloop(Pool, Elect1);
    %% add new backend
    {be_add,Server} ->
      gloop([Server|Pool], Elect);
    %% remove some backend
    {be_del,Server} ->
      case lists:member(Server, Pool) of
      true ->
        gloop(lists:delete(Server, Pool),Elect);
      false ->
        gloop(Pool, Elect)
      end;
    %% list all backends
    {Client,list_be} ->
      Client ! {ok, Pool},
      gloop(Pool, Elect);
    %% resend error from the backend to the client
    %% hide the server Pid, return Gw Pid
    {Client, crash} ->
      Client ! {self(), crash},
      gloop(Pool, Elect);
    %% send the data to the elected backend
    %  check for empty BE pool
    %  the election function must check the backends
    %% for availability
    {Client, Data} ->
      %% not empty pool
      if Pool /= [] -> 
        case catch Elect(Pool) of
          {empty,[]} ->
            Client ! {self(), mustwait},
            gloop(Pool, Elect);
          {'EXIT', Why} ->
            Client ! {self(), Why, mustwait},
            gloop(Pool, Elect);
          {Backend,Pool1} ->
            Backend ! {self(), Client, Data},
            gloop(Pool1,Elect)
        end;
       %% empty pool
       true ->
         Client ! {self(), mustwait},
         gloop(Pool, Elect)
       end;
    %% resend the response to the client
    %% replace the backend Pid with the Gw one
    {Client, ok, Data} ->
      Client ! {self(), Data},
      gloop(Pool, Elect)
  end.